package polimi.reds.optimizer.algorithm;

import java.util.ArrayList;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import polimi.reds.Filter;
import polimi.reds.Message;
import polimi.reds.NodeDescriptor;
import polimi.reds.broker.overlay.Overlay;
import polimi.reds.broker.routing.Router;
import polimi.reds.broker.routing.SubscriptionTable;
import polimi.reds.optimizer.AbstractOptimizerStrategy;
import polimi.reds.optimizer.TrafficLock;

public class SATOptimizerStrategy extends AbstractOptimizerStrategy {

	private SubscriptionTable st=null;
	
	// Traffic counters, each traffic key represents a filter and a node
	private Map<TrafficKey, Double> trafficMap = new HashMap<TrafficKey, Double>();
	
	private long updateEvents;
	private long eventsBeforeUpdate;
	private Random rnd;
	private String id;
	private int threshold;
	
	@Override
	public void initialize(Overlay overlay, Router router, Map<String,String> options, TrafficLock trafficLock) {
		super.initialize(overlay, router, options, trafficLock);
		rnd=new Random(System.currentTimeMillis()+overlay.getNodeDescriptor().hashCode());
		this.id = overlay.getNodeDescriptor().getID();
		this.updateEvents = Integer.parseInt(options.get(PARAM_UPDATEEVENTS));
		this.eventsBeforeUpdate = Integer.parseInt(options.get(PARAM_UPDATEEVENTS));
		this.threshold = Integer.parseInt(options.get(PARAM_THRESHOLD));
	}
		
	private void tryReconfiguration() {
		if (eventsBeforeUpdate>0) { // || getOverlay().getNumberOfBrokers()>maxNeighbors) {
			return;
		}
	
		List<NodeDescriptor> nodesToRewire = chooseNodesToRewire();
		
		if (nodesToRewire == null) {
			logger.fine(id + " has no broker neighbors to rewire. (from a total of " + getOverlay().getNumberOfBrokers() + " neighbors)");
			return;
		}
		
		//Double savedMessages = testNeighbors(nodesToRewire.get(0), nodesToRewire.get(1));
		tryToSwapNeighbor(nodesToRewire.get(0), getOverlay().getNodeDescriptor(),nodesToRewire.get(1));		
		eventsBeforeUpdate = (long) (updateEvents*(50.0+rnd.nextDouble()*100.0)/100.0);
	}

	private List<NodeDescriptor> chooseNodesToRewire() {
		List<NodeDescriptor> rNeighbors = new ArrayList<NodeDescriptor>(getOverlay().getNeighbors());		
		Collections.shuffle(rNeighbors,rnd);
		
		
		List<NodeDescriptor> nodesToRewire = new ArrayList<NodeDescriptor>();
		
		NodeDescriptor n1 = null;
		NodeDescriptor n2 = null;
		Double max = 0.0;
		Double tmp = 0.0;
		
		for (Integer i=0; i<rNeighbors.size(); i++) {
			if (rNeighbors.get(i).isClient() || rNeighbors.get(i).equals(getOverlay().getNodeDescriptor())) {
				continue;
			}
			//System.out.print("-");
			for (Integer j=0; j<rNeighbors.size(); j++) {
				if (rNeighbors.get(j).isClient() || (i==j) || rNeighbors.get(j).equals(this)) {
					continue;
				}
				//System.out.print(".");
				
				tmp = evalutateReconfiguration(rNeighbors.get(i), rNeighbors.get(j));
				if (tmp > max - threshold) {
					max = tmp;
				    n1 = rNeighbors.get(i);
				    n2 = rNeighbors.get(j);
				}
			}
		}
		
		if ((n1 == null) || (n2 == null)) {
			return null;
		}
		
		nodesToRewire.add(n1);
		nodesToRewire.add(n2);
		
		logger.fine(id + ": rewiring -(" + getOverlay().getNodeDescriptor().getID() + 
				"," + n1.getID() + ") +(" + n1.getID() + "," + n2.getID() + ") will save " + max + 
				" messages (> threshold=" + (0-threshold) + ")");	
		return nodesToRewire;
	}

	@SuppressWarnings("unchecked")
	private Double evalutateReconfiguration(NodeDescriptor n1,
			NodeDescriptor n2) {
		
		if (n1.isClient() || n2.isClient()) {
			return Double.MIN_VALUE;
		}
		
		String debugString = "["+id+"] Testing " + n1.getID() + " and " + n2.getID() + " ";
		
		Double savedMessages = 0.0;
		Collection<Filter> allFilters = st.getAllFilters(false);
		
		for (Filter f:allFilters) {

			Double u_fa = trafficMap.get(new TrafficKey(f,true,n1));
			Double v_fc = trafficMap.get(new TrafficKey(f,true,n2));
			Double xfa = trafficMap.get(new TrafficKey(f,false,n1));
			Double yfc = trafficMap.get(new TrafficKey(f,false,n2));
			
			Double a = 0.0;
			Double b = 0.0;
			
			Double s_fs = 0.0;
			Double zfz = 0.0;
			
			if (u_fa == null) {
				u_fa = 0.0;
			}
			
			if (v_fc == null) {
				v_fc = 0.0;
			}
			
			if (xfa == null) {
				xfa = 0.0;
			}
			
			if (yfc == null) {
				yfc = 0.0;
			}
			
			debugString += "(F=" + f;
			
			// check neighbors
			for (NodeDescriptor n:getOverlay().getNeighbors()) {
				if (n.equals(n1) || n.equals(n2)) {
					continue;
				}
				
				Double temp = trafficMap.get(new TrafficKey(f,true,n));
				
				if (temp == null) {
					temp = 0.0;
				}
				
				s_fs += temp;
				
				temp = trafficMap.get(new TrafficKey(f,false,n));
				
				if (temp == null) {
					temp = 0.0;
				}
				zfz += temp;
			}
			
			if (zfz > 0) {
				a = u_fa + v_fc;
			}
			
			if (xfa > 0 || yfc > 0) {
				b = s_fs;
			}
			
			debugString += " u_fa=" + u_fa + " xfa=" + xfa + " " + " vfc=" + v_fc + " yfc=" + yfc + " a=" + a + " b=" + b + " zfs=" + zfz + 
						   " s_fs=" + s_fs + " saving="+ (v_fc + yfc - a - b) + ") ";
			savedMessages += v_fc + yfc - a - b;
		}
		
		debugString += " " + savedMessages + " expected saved messages";
		logger.fine(debugString);
		
		return savedMessages;
	}

	@Override
	protected void brokersUpdateEvent(Collection<NodeDescriptor> nodes) {
		
		if (nodes.size() == 0) {
			return;
		}
		
		//Collection<TrafficKey> knownKeys = new ArrayList<TrafficKey>(trafficMap.keySet());
		//
		//for (TrafficKey k:knownKeys) {
		//	if (!nodes.contains(k.getNodeDescriptor())) {
		//		trafficMap.remove(k.getNodeDescriptor());
		//	}
		//}
		trafficMap.clear();
		
		
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void postRoutingTrafficEvent(NodeDescriptor source,
			NodeDescriptor destination, Message data) {

		if (st==null) {
			return;
		}
		
		Collection<Filter> filters = st.getAllFilters(destination); //TODO
		
		if ((filters==null) || filters.size()==0) {
			return;
		}
		
		for (Filter f:filters) {
			if (f.matches(data)) {
				TrafficKey trafficKey = new TrafficKey(f,false,destination);
				Double count=trafficMap.get(trafficKey);
				if (count==null) {
					count=0.0;
				}
				count+=1/filters.size();
				trafficMap.put(trafficKey, count);
			}
		}
		
		eventsBeforeUpdate--;
		tryReconfiguration();
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void preRoutingTrafficEvent(NodeDescriptor source, Message data) {
		if (st==null) {
			return;
		}
		
		Collection<Filter> filters = st.getAllFiltersExcept(false, source); //.getAllFilters(source);
		
		if ((filters == null) || filters.size()==0) {
			return;
		}
		
		for (Filter f:filters) {
			if (f.matches(data)) {
				TrafficKey trafficKey = new TrafficKey(f,true,source);
				Double count=trafficMap.get(trafficKey);
				if (count==null) {
					count=0.0;
				}
				count+=1/filters.size();
				trafficMap.put(trafficKey, count);
			}
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void subscriptionTableUpdateEvent(
			SubscriptionTable subscriptionTable) {
		st=subscriptionTable;
		
		if (trafficMap == null || trafficMap.keySet().size() == 0) {
			return;
		}
		
		Collection<TrafficKey> knownKeys = new ArrayList<TrafficKey>(trafficMap.keySet());
		Collection<Filter> filters = st.getAllFilters(false);
		
		for (TrafficKey k:knownKeys) {
			if (!filters.contains(k.getFilter())) {
				trafficMap.remove(k.getNodeDescriptor());
			}
		}
	}
	
	/**
	 * This class represent a traffic key that is used in the traffic hashmap.
	 * A traffic key identified the source node and the destination node of
	 * a traffic unit. For local clients we use the local node.
	 * 
	 * @author daniel
	 *
	 */
	private class TrafficKey {
		
		private Filter filter;
		private Boolean from;
		private NodeDescriptor node;

		/**
		 * Creates a new traffic key for the specified filter.
		 * 
		 * @param filter Filter associated to the traffic
		 * @param from True if the message is an incoming message, False if it is an outgoing message
		 * @param node Source or Destination (depends on from)
		 */
		public TrafficKey(Filter filter, Boolean from, NodeDescriptor node) {
			this.filter = filter;
			this.from = from;
			
			if ((node==null) || (node.isClient())) {
				this.node = null;
			} else {
				this.node = node;
			}
		}
		
		/**
		 * String representation of the traffic key
		 */
		public String toString() {
			return "{F=" + filter + (from ? " in " : " out ") + "N=" + node + "}";
		}
		
		/**
		 * Get the filter for this traffic key
		 * @return
		 */
		public Filter getFilter() {
			return filter;
		}
		
		/**
		 * Get the direction of the message.
		 * True means incoming message, False means outgoing message
		 * 
		 * @return The direction of the message (incoming=true, outgoing=false)
		 */
		//public Boolean getFrom() {
		//	return from;
		//}
		
		/**
		 * Get the sender/recipient of the traffic associated to this traffic key
		 * 
		 * @return The remote node involved in this traffic
		 */
		public NodeDescriptor getNodeDescriptor() {
			return node;
		}
		
		public int hashCode() {
			return toString().hashCode();
		}
		
		public boolean equals(Object obj) {
			if (!(obj instanceof TrafficKey)) {
				return super.equals(obj);
			}
			
			return obj.toString().equals(toString());
		}
	}

}
